EC2 Instance Connect Endpoint 経由で RDS for PostgreSQL に接続する構成を AWS CDK で構築してみた

EC2 Instance Connect Endpoint 経由で RDS for PostgreSQL に接続する構成を AWS CDK で構築してみた

Clock Icon2024.10.02

こんにちは、製造ビジネステクノロジー部の若槻です。

前回のエントリでは、EC2 Instance Connect Endpoint でプライベートサブネット内の EC2 Instance に踏み台ホストを使わずに接続する構成を AWS CDK で作成してみました。
https://dev.classmethod.jp/articles/aws-cdk-ec2-instance-connect-endpoint-with-privatewithegress-ec2-instance/

今回は、EC2 Instance Connect Endpoint で接続した EC2 Instance から、さらに Amazon RDS for PostgreSQL Instance に接続する構成を AWS CDK で構築してみました。

試してみた

今回試した構成は下記のようになります。

CDK コード

全体の CDK コードは下記のようになります。

lib/sample-app-stack.ts
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as rds from 'aws-cdk-lib/aws-rds';
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';

export class SampleAppStack extends cdk.Stack {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    // VPC の作成
    const vpc = new ec2.Vpc(this, 'VPC', {
      subnetConfiguration: [
        // NAT Gateway を配置するパブリックサブネット
        {
          cidrMask: 24,
          name: 'Public',
          subnetType: ec2.SubnetType.PUBLIC,
        },
        // EC2 Instance を配置するプライベートサブネット
        // インスタンスから外部への通信を許可するために Egress ルートを追加
        {
          cidrMask: 24,
          name: 'PrivateWithEgress',
          subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
        },
        // EC2 Instance Connect Endpoint および RDS Instance を配置するプライベートサブネット
        {
          cidrMask: 24,
          name: 'PrivateIsolated',
          subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
        },
      ],
    });

    /**
     * セキュリティグループの作成
     */

    // EC2 Instance Connect Endpoint に関連付けるセキュリティグループ
    const instanceConnectEndpointSecurityGroup = new ec2.SecurityGroup(
      this,
      'InstanceConnectEndpointSecurityGroup',
      {
        vpc,
        allowAllOutbound: false,
      }
    );

    // EC2 Instance に関連付けるセキュリティグループ
    const ec2InstanceSecurityGroup = new ec2.SecurityGroup(
      this,
      'Ec2InstanceSecurityGroup',
      {
        vpc,
        allowAllOutbound: false,
      }
    );

    // RDS Instance に関連付けるセキュリティグループ
    const rdsInstanceSecurityGroup = new ec2.SecurityGroup(
      this,
      'RdsInstanceSecurityGroup',
      {
        vpc,
        allowAllOutbound: false,
      }
    );

    /**
     * EC2 Instance Connect Endpoint を作成
     */
    new ec2.CfnInstanceConnectEndpoint(this, 'InstanceConnectEndpoint', {
      subnetId: vpc.selectSubnets({
        subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
      }).subnetIds[0],
      securityGroupIds: [instanceConnectEndpointSecurityGroup.securityGroupId],
    });

    /**
     * EC2 Instance と EC2 Instance Connect Endpoint 間の通信を許可するためのルールを設定
     */

    // EC2 Instance Connect Endpoint から EC2 Instance への通信を許可するための Ingress ルール
    ec2InstanceSecurityGroup.addIngressRule(
      instanceConnectEndpointSecurityGroup,
      ec2.Port.tcp(22)
    );

    // EC2 Instance Connect Endpoint から EC2 Instance への通信を許可するための Egress ルール
    instanceConnectEndpointSecurityGroup.addEgressRule(
      ec2InstanceSecurityGroup,
      ec2.Port.tcp(22)
    );

    /**
     * EC2 Instance と RDS Instance 間の通信を許可するためのルールを設定
     */

    // EC2 Instance から RDS Instance への通信を許可するための Ingress ルール
    rdsInstanceSecurityGroup.addIngressRule(
      ec2InstanceSecurityGroup,
      ec2.Port.tcp(5432)
    );

    // EC2 Instance から RDS Instance への通信を許可するための Egress ルール
    ec2InstanceSecurityGroup.addEgressRule(
      rdsInstanceSecurityGroup,
      ec2.Port.tcp(5432)
    );

    /**
     * EC2 Instance および関連リソースを作成
     */

    // EC2 Instance を作成
    const ec2Instance = new ec2.Instance(this, 'Ec2Instance', {
      vpc,
      vpcSubnets: {
        subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
      },
      instanceType: ec2.InstanceType.of(
        ec2.InstanceClass.T2,
        ec2.InstanceSize.MICRO
      ),
      machineImage: new ec2.AmazonLinuxImage({
        generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2023,
      }),
      securityGroup: ec2InstanceSecurityGroup,
    });

    // EC2 Instance からの Egress 通信は 443 ポートのみ許可
    ec2InstanceSecurityGroup.addEgressRule(
      ec2.Peer.anyIpv4(),
      ec2.Port.tcp(443)
    );

    // ユーザーデータとして psql のインストールコマンドを追加
    ec2Instance.userData.addCommands('sudo dnf install -y postgresql15');

    // ユーザーデータに cfn-signal コマンドを追加
    ec2Instance.userData.addSignalOnExitCommand(ec2Instance);

    /**
     * RDS Instance を作成
     */
    new rds.DatabaseInstance(this, 'RdsInstance', {
      engine: rds.DatabaseInstanceEngine.postgres({
        version: rds.PostgresEngineVersion.VER_16_4,
      }),
      vpc,
      vpcSubnets: {
        subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
      },
      securityGroups: [rdsInstanceSecurityGroup],
      instanceType: ec2.InstanceType.of(
        ec2.InstanceClass.T3,
        ec2.InstanceSize.MICRO
      ),
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });
  }
}

今回は RDS for PostgreSQL Instance への接続は psql を使って行うので、ユーザーデータで EC2 Instance に psql の事前インストールを行っています。また psql のインストールができるように EC2 Instance にポート 443 の Egress ルートを追加しています。

上記をデプロイすると下記のような CloudFormation スタックが作成されます。

Secrets Manager に保存された RDS の接続情報を確認

CDK の DatabaseInstance Construct クラスでは、RDS の接続情報が保存される Secrets Manager シークレットが既定で作成されます。

作成されたシークレットには CloudFormation スタックのリソース一覧からアクセスすることができます。

シークレットに各種接続情報が保存されていることを確認します。

EC2 Instance Connect Endpoint 経由で RDS for PostgreSQL に接続

EC2 Instance に EC2 Instance Connect Endpoint を使用して SSH 接続します。

シークレットで確認した HOST、PORT、USER_NAME および PGPASSWORD を使用して、RDS for PostgreSQL に接続します。下記は psql での接続に成功し、データベース一覧が確認できた様子です。

$ HOST=sampleapp-rdsinstance1d827d17-63gragyxdmr2.c5v2lejuonkv.ap-northeast-1.rds.amazonaws.com
$ PORT=5432
$ USER_NAME=postgres
$ export PGPASSWORD=ZcI...
$ psql -h $HOST -p $PORT -U $USER_NAME
psql (15.8, server 16.4)
WARNING: psql major version 15, server major version 16.
         Some psql features might not work.
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off)
Type "help" for help.

postgres=> \l
                                                 List of databases
   Name    |  Owner   | Encoding |   Collate   |    Ctype    | ICU Locale | Locale Provider |   Access privileges
-----------+----------+----------+-------------+-------------+------------+-----------------+-----------------------
 postgres  | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 |            | libc            |
 rdsadmin  | rdsadmin | UTF8     | en_US.UTF-8 | en_US.UTF-8 |            | libc            | rdsadmin=CTc/rdsadmin
 template0 | rdsadmin | UTF8     | en_US.UTF-8 | en_US.UTF-8 |            | libc            | =c/rdsadmin          +
           |          |          |             |             |            |                 | rdsadmin=CTc/rdsadmin
 template1 | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 |            | libc            | =c/postgres          +
           |          |          |             |             |            |                 | postgres=CTc/postgres
(4 rows)

ここで RDS Instance をダッシュボードから確認すると、Connection が 1 に増えていることが確認できます。

接続中のコンピューティングリソース一覧には表示されない

RDS Instance のダッシュボードから接続中のコンピューティングリソース(Connected compute resources)一覧を確認すると、接続中であるはずの EC2 Instance が表示されていません。

これは Instance に関連付けられているセキュリティグループの名前のパターンが ec2-rds-n および rds-ec2-n である場合にのみ表示されるためです。今回はそれぞれ Ec2InstanceSecurityGroup および RdsInstanceSecurityGroup という名前で作成しているため表示されていません。

https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/UserGuide/ec2-rds-connect.html

おわりに

EC2 Instance Connect Endpoint 経由で RDS for PostgreSQL に接続する構成を AWS CDK で構築してみました。

RDS を使用する場合はメンテナンスなどの用途でデータベースに接続可能したい場合がほとんどです。その構成をマネジメントコンソールからではなく CDK で IaC 管理しつつ構築することができるのは便利だと思います。

参考

https://aws.amazon.com/rds/instance-types/
https://dev.classmethod.jp/articles/using-cfn-signal-to-wait-for-user-data-execution-in-aws-cdk-ec2-instance-setup/

以上

この記事をシェアする

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.